import { Injectable, Logger, OnModuleDestroy, OnModuleInit, Inject } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis from 'ioredis';
import { v4 as uuidv4 } from 'uuid';
import * as crypto from 'crypto';

interface SessionData {
  userId: string;
  deviceSerialNumber: string | null;
  refreshToken: string | null;
  createdAt: number;
}

@Injectable()
export class RedisService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(RedisService.name);
  private readonly sessionPrefix = 'session:';
  private readonly verificationPrefix = 'password_reset:';
  private readonly sessionExpiry: number; // in seconds
  private readonly refreshTokenExpiry: number = 2592000; // 30 days in seconds

  constructor(
    private readonly configService: ConfigService,
    @Inject('REDIS_CLIENT') private readonly client: Redis,
  ) {
    this.sessionExpiry = this.refreshTokenExpiry; // Set session expiry to match refresh token expiry

    this.client.on('error', (error) => {
      this.logger.error('Redis connection error:', error);
    });

    this.client.on('connect', () => {
      this.logger.log('Connected to Redis');
    });
  }

  /**
   * Initialize Redis connection when the module is initialized
   */
  async onModuleInit() {
    try {
      this.logger.log('Redis service initialized');
    } catch (error) {
      this.logger.error('Failed to initialize Redis service:', error);
      throw error;
    }
  }

  /**
   * Create a new session
   * @param userId User ID
   * @param deviceSerialNumber Device serial number (optional for admin sessions)
   * @param refreshToken Refresh token (optional for admin sessions)
   * @returns Session ID
   */
  async createSession(
    userId: string,
    deviceSerialNumber: string | null,
    refreshToken: string | null,
  ): Promise<string> {
    const sessionId = uuidv4();
    const key = this.getSessionKey(sessionId);

    const sessionData: SessionData = {
      userId,
      deviceSerialNumber,
      refreshToken,
      createdAt: Date.now(),
    };

    // Use refresh token expiry if refresh token is provided, otherwise use session expiry
    const expiry = refreshToken ? this.refreshTokenExpiry : this.sessionExpiry;
    await this.set(key, JSON.stringify(sessionData), expiry);

    this.logger.debug(`Created session ${sessionId} for user ${userId}`);
    return sessionId;
  }

  /**
   * Get session data
   * @param sessionId Session ID
   * @returns Session data or null if not found
   */
  async getSession(sessionId: string): Promise<SessionData | null> {
    const key = this.getSessionKey(sessionId);
    const data = await this.get(key);

    if (!data) {
      return null;
    }

    try {
      const sessionData = JSON.parse(data) as SessionData;
      return sessionData;
    } catch (error) {
      this.logger.error(`Error parsing session data: ${error.message}`);
      return null;
    }
  }

  /**
   * Delete a session
   * @param sessionId Session ID
   * @returns True if session was deleted, false if not found
   */
  async deleteSession(sessionId: string): Promise<boolean> {
    const key = this.getSessionKey(sessionId);
    const result = await this.del(key);

    const deleted = result === 1;
    if (deleted) {
      this.logger.debug(`Deleted session ${sessionId}`);
    } else {
      this.logger.debug(`Session ${sessionId} not found for deletion`);
    }

    return deleted;
  }

  /**
   * Validate a refresh token
   * @param sessionId Session ID
   * @param refreshToken Refresh token
   * @returns True if valid, false otherwise
   */
  async validateRefreshToken(sessionId: string, refreshToken: string): Promise<boolean> {
    const session = await this.getSession(sessionId);

    if (!session || !session.refreshToken) {
      return false;
    }

    // Validate refresh token
    const isValid = session.refreshToken === refreshToken;

    if (isValid) {
      this.logger.debug(`Validated refresh token for session ${sessionId}`);
    } else {
      this.logger.warn(`Invalid refresh token for session ${sessionId}`);
    }

    return isValid;
  }

  /**
   * Find a session by refresh token
   * @param refreshToken Refresh token,
   * @returns Session ID and data if found, null otherwise
   */
  async findSessionByRefreshToken(
    refreshToken: string,
  ): Promise<{ sessionId: string; session: SessionData } | null> {
    // Get all sessions
    const allSessionKeys = await this.client.keys(`${this.sessionPrefix}*`);

    // For each session, check if it has the matching refresh token
    for (const key of allSessionKeys) {
      const sessionData = await this.get(key);
      if (sessionData) {
        try {
          const parsedData = JSON.parse(sessionData) as SessionData;
          if (parsedData.refreshToken === refreshToken) {
            const sessionId = key.replace(this.sessionPrefix, '');
            return { sessionId, session: parsedData };
          }
        } catch (error) {
          this.logger.error(`Error parsing session data: ${error.message}`);
        }
      }
    }

    return null;
  }

  /**
   * Generate a refresh token
   * @returns Refresh token
   */
  generateRefreshToken(): string {
    return crypto.randomBytes(40).toString('hex');
  }

  /**
   * Get the Redis key for a session
   * @param sessionId Session ID
   * @returns Redis key
   */
  private getSessionKey(sessionId: string): string {
    return `${this.sessionPrefix}${sessionId}`;
  }

  /**
   * Delete all sessions for a user
   * @param userId User ID
   * @returns Number of sessions deleted
   */
  async deleteUserSessions(userId: string): Promise<number> {
    // Get all sessions
    const allSessionKeys = await this.client.keys(`${this.sessionPrefix}*`);
    let deletedCount = 0;

    // For each session, check if it belongs to the user
    for (const key of allSessionKeys) {
      const sessionData = await this.get(key);
      if (sessionData) {
        try {
          const parsedData = JSON.parse(sessionData) as SessionData;
          if (parsedData.userId === userId) {
            await this.del(key);
            deletedCount++;
          }
        } catch (error) {
          this.logger.error(`Error parsing session data: ${error.message}`);
        }
      }
    }

    if (deletedCount > 0) {
      this.logger.log(`Deleted ${deletedCount} sessions for user ${userId}`);
    }

    return deletedCount;
  }

  /**
   * Get a value from Redis
   * @param key Redis key
   * @returns Value or null if not found
   */
  async get(key: string): Promise<string | null> {
    try {
      return await this.client.get(key);
    } catch (error) {
      this.logger.error(`Error getting key ${key}:`, error);
      return null;
    }
  }

  /**
   * Set a value in Redis with expiry
   * @param key Redis key
   * @param value Value to set
   * @param expiry Expiry time in seconds
   * @returns OK if successful
   */
  async set(key: string, value: string, expirySeconds?: number): Promise<void> {
    try {
      if (expirySeconds) {
        await this.client.setex(key, expirySeconds, value);
        this.logger.debug(`Set key ${key} with expiry ${expirySeconds}s`);
      } else {
        await this.client.set(key, value);
        this.logger.debug(`Set key ${key} without expiry`);
      }
    } catch (error) {
      this.logger.error(`Error setting key ${key}:`, error);
      throw error;
    }
  }

  /**
   * Delete a key from Redis
   * @param key Redis key
   * @returns Number of keys deleted
   */
  async del(key: string): Promise<number> {
    try {
      return await this.client.del(key);
    } catch (error) {
      this.logger.error(`Error deleting key ${key}:`, error);
      return 0;
    }
  }

  /**
   * Get keys matching a pattern
   * @param pattern Pattern to match (e.g. 'user_session:*')
   * @returns Array of matching keys
   */
  async getKeys(pattern: string): Promise<string[]> {
    try {
      return await this.client.keys(pattern);
    } catch (error) {
      this.logger.error(`Error getting keys with pattern ${pattern}:`, error);
      return [];
    }
  }

  /**
   * Clean up Redis connection on module destroy
   */
  async onModuleDestroy() {
    try {
      await this.client.quit();
      this.logger.log('Redis connection closed');
    } catch (error) {
      this.logger.error('Error closing Redis connection:', error);
    }
  }

  /**
   * Close Redis connection manually
   * This is useful for tests
   */
  async closeConnection() {
    if (this.client) {
      try {
        // Check if the client is still connected
        if (this.client.status === 'ready') {
          await this.client.quit();
          this.logger.log('Redis connection closed manually');
        } else {
          this.logger.log('Redis connection already closed');
        }
      } catch (error) {
        this.logger.error('Error closing Redis connection:', error);
      }
    }
  }
}
